⚡ 30 秒速记
- 核心判断:虚拟节点是对界面结构的描述,渲染器通过挂载与更新把声明式描述映射为宿主环境操作
- 原理主线:围绕 「减小DOM操作的性能开销」、「尽可能的复用 DOM 元素」、「另一个思路 - 双端比较」 建立输入、状态变化与输出之间的因果关系
- 文章范围:解析Vue渲染器的核心Diff算法,讲解其在高效更新DOM、优化性能、处理多子节点时的原理与实现方式,帮助开发者理解和提升前端渲染效率。
- 边界与代价:
key、节点类型和稳定序列决定复用边界;虚拟 DOM 解决可预测更新,不保证永远比手写 DOM 快 - 工程落地:性能分析要看实际提交次数、节点移动和组件边界,不能只用“减少 DOM 操作”解释
核心 Diff 处理的是新旧两组子节点,目标是尽可能复用和移动 DOM,而不是全部删除后重建。 没有 key 时,只能按位置比较公共长度,多出的新节点执行挂载,多出的旧节点执行移除;列表换序时,这可能把本可复用的节点内容逐个改写。加入稳定且唯一的 key 后,渲染器才能建立新旧节点的映射,先调用 patch 保证内容正确,再根据旧索引是否保持递增判断哪些节点需要移动。key 解决的是身份识别,不代表节点一定无需更新。
这篇文章不要按 API 清单来背。先用上面的 Mind Map 建立全局结构,再通过交互 DEMO 观察正常路径和边界路径如何改变状态;阅读正文时重点核对每一步的输入、负责执行的参与者、产生的中间状态以及最终可观察结果。遇到版本敏感结论,要把“历史实现”“当前行为”和“工程兼容策略”分开说明;遇到性能或架构取舍,则用实际指标、失败现象和验证手段支撑判断。
版本校准: 本文出现 Object.defineProperty、Dep、Watcher 和双端 Diff 时,主要描述 Vue 2 实现;Vue 3 使用 Proxy、effect 与新的渲染器路径。Vue 2 已于 2023-12-31 结束维护,新项目应以 Vue 3 为基线,旧项目参考 Vue 2 EOL 官方说明 制定迁移与安全策略。
# 减小DOM操作的性能开销
上一章我们讨论了渲染器是如何更新各种类型的 VNode 的,实际上,上一章所讲解的内容归属于完整的 Diff 算法之内,但并不包含核心的 Diff 算法。那什么才是核心的 Diff 算法呢?看下图:

我们曾在上一章中讲解子节点更新的时候见到过这张图,当时我们提到只有当新旧子节点的类型都是多个子节点时,核心 Diff 算法才派得上用场,并且当时我们采用了一种仅能实现目标但并不完美的算法:遍历旧的子节点,将其全部移除;再遍历新的子节点,将其全部添加,如下高亮代码所示:
function patchChildren(
prevChildFlags,
nextChildFlags,
prevChildren,
nextChildren,
container
) {
switch (prevChildFlags) {
// 省略...
// 旧的 children 中有多个子节点
default:
switch (nextChildFlags) {
case ChildrenFlags.SINGLE_VNODE:
// 省略...
case ChildrenFlags.NO_CHILDREN:
// 省略...
default:
// 新的 children 中有多个子节点
// 遍历旧的子节点,将其全部移除
for (let i = 0; i < prevChildren.length; i++) {
container.removeChild(prevChildren[i].el)
}
// 遍历新的子节点,将其全部添加
for (let i = 0; i < nextChildren.length; i++) {
mount(nextChildren[i], container)
}
break
}
break
}
}